import _ from "underscore";
import jQuery from "jquery";
import Backbone from "backbone";
import { getAppRoot } from "onload/loadConfig";
import STATES from "mvc/dataset/states";
import BASE_MVC from "mvc/base-mvc";
import _l from "utils/localization";
import TAB_UPDATES from "mvc/user/tab-updates";

var logNamespace = "dataset";
var hiddenupdates = 1;
//==============================================================================
var searchableMixin = BASE_MVC.SearchableModelMixin;
/** @class base model for any DatasetAssociation (HDAs, LDDAs, DatasetCollectionDAs).
 *      No knowledge of what type (HDA/LDDA/DCDA) should be needed here.
 *  The DA's are made searchable (by attribute) by mixing in SearchableModelMixin.
 */
var DatasetAssociation = Backbone.Model.extend(BASE_MVC.LoggableMixin).extend(
    BASE_MVC.mixin(
        searchableMixin,
        /** @lends DatasetAssociation.prototype */ {
            _logNamespace: logNamespace,

            /** default attributes for a model */
            defaults: {
                state: STATES.NEW,
                deleted: false,
                purged: false,
                name: "(unnamed dataset)",
                accessible: true,
                // sniffed datatype (sam, tabular, bed, etc.)
                data_type: "",
                file_ext: "",
                file_size: 0,

                // array of associated file types (eg. [ 'bam_index', ... ])
                meta_files: [],

                misc_blurb: "",
                misc_info: "",

                tags: [],
                // do NOT default on annotation, as this default is valid and will be passed on 'save'
                //  which is incorrect behavior when the model is only partially fetched (annos are not passed in summary data)
                //annotation          : ''
            },

            /** instance vars and listeners */
            initialize: function (attributes, options) {
                this.debug(`${this}(Dataset).initialize`, attributes, options);

                //!! this state is not in trans.app.model.Dataset.states - set it here -
                if (!this.get("accessible")) {
                    this.set("state", STATES.NOT_VIEWABLE);
                }

                /** Datasets rely/use some web controllers - have the model generate those URLs on startup */
                this.urls = this._generateUrls();

                this._setUpListeners();
            },

            _getDatasetId: function () {
                return this.get("id");
            },

            /** returns misc. web urls for rendering things like re-run, display, etc. */
            _generateUrls: function () {
                const id = this._getDatasetId();
                if (!id) {
                    return {};
                }
                var urls = {
                    purge: `datasets/${id}/purge_async`,
                    display: `datasets/${id}/display/?preview=True`,
                    edit: `datasets/edit?dataset_id=${id}`,
                    download: `datasets/${id}/display${this._downloadQueryParameters()}`,
                    report_error: `dataset/errors?id=${id}`,
                    rerun: `tool_runner/rerun?id=${id}`,
                    show_params: `datasets/${id}/show_params`,
                    visualization: "visualization",
                    meta_download: `dataset/get_metadata_file?hda_id=${id}&metadata_name=`,
                };
                _.each(urls, (value, key) => {
                    urls[key] = getAppRoot() + value;
                });
                this.urls = urls;
                return urls;
            },

            _downloadQueryParameters: function () {
                return `?to_ext=${this.get("file_ext")}`;
            },

            /** set up any event listeners
             *  event: state:ready  fired when this DA moves into/is already in a ready state
             */
            _setUpListeners: function () {
                // if the state has changed and the new state is a ready state, fire an event
                this.on("change:state", function (currModel, newState) {
                    this.log(`${this} has changed state:`, currModel, newState);
                    if (this.inReadyState()) {
                        this.trigger("state:ready", currModel, newState, this.previous("state"));
                        if (newState != "discarded") {
                            if (newState === "ok") {
                                new Notification(`Job complete: ${this.get("name")}`, {
                                    icon: "static/favicon.ico",
                                });
                                if (TAB_UPDATES.is_hidden()) {
                                    TAB_UPDATES.hidden_count(hiddenupdates);
                                    hiddenupdates++;
                                }
                            } else if (newState == "error") {
                                new Notification(`Job failure: ${this.get("name")}`, {
                                    icon: "static/erricon.ico",
                                });
                                if (TAB_UPDATES.is_hidden() && Notification.permission == "granted") {
                                    TAB_UPDATES.change_favicon("static/erricon.ico");
                                }
                            }
                        }
                    }
                });
                // the download url (currently) relies on having a correct file extension
                this.on("change:id change:file_ext", function (currModel) {
                    this._generateUrls();
                });
            },

            // ........................................................................ common queries
            /** override to add urls */
            toJSON: function () {
                var json = Backbone.Model.prototype.toJSON.call(this);
                //console.warn( 'returning json?' );
                //return json;
                return _.extend(json, {
                    urls: this.urls,
                });
            },

            /** Is this dataset deleted or purged? */
            isDeletedOrPurged: function () {
                return this.get("deleted") || this.get("purged");
            },

            /** Is this dataset in a 'ready' state; where 'Ready' states are states where no
             *      processing (for the ds) is left to do on the server.
             */
            inReadyState: function () {
                var ready = _.contains(STATES.READY_STATES, this.get("state"));
                return this.isDeletedOrPurged() || ready;
            },

            /** Does this model already contain detailed data (as opposed to just summary level data)? */
            hasDetails: function () {
                // if it's inaccessible assume it has everything it needs
                if (!this.get("accessible")) {
                    return true;
                }
                return this.has("annotation");
            },

            /** Convenience function to match dataset.has_data. */
            hasData: function () {
                return this.get("file_size") > 0;
            },

            // ........................................................................ ajax
            fetch: function (options) {
                var dataset = this;
                return Backbone.Model.prototype.fetch.call(this, options).always(() => {
                    dataset._generateUrls();
                });
            },

            /** override to use actual Dates objects for create/update times */
            parse: function (response, options) {
                var parsed = Backbone.Model.prototype.parse.call(this, response, options);
                if (parsed.create_time) {
                    parsed.create_time = new Date(parsed.create_time);
                }
                if (parsed.update_time) {
                    parsed.update_time = new Date(parsed.update_time);
                }
                return parsed;
            },

            /** override to wait by default */
            save: function (attrs, options) {
                options = options || {};
                options.wait = _.isUndefined(options.wait) ? true : options.wait;
                return Backbone.Model.prototype.save.call(this, attrs, options);
            },

            //NOTE: subclasses of DA's will need to implement url and urlRoot in order to have these work properly
            /** save this dataset, _Mark_ing it as deleted (just a flag) */
            delete: function (options) {
                if (this.get("deleted")) {
                    return jQuery.when();
                }
                return this.save({ deleted: true }, options);
            },
            /** save this dataset, _Mark_ing it as undeleted */
            undelete: function (options) {
                if (!this.get("deleted") || this.get("purged")) {
                    return jQuery.when();
                }
                return this.save({ deleted: false }, options);
            },

            /** remove the file behind this dataset from the filesystem (if permitted) */
            purge: function _purge(options) {
                //TODO: use, override model.destroy, HDA.delete({ purge: true })
                if (this.get("purged")) {
                    return jQuery.when();
                }
                options = options || {};
                options.url = this.urls.purge;

                //TODO: ideally this would be a DELETE call to the api
                //  using purge async for now
                var hda = this;

                var xhr = jQuery.ajax(options);
                xhr.done((message, status, responseObj) => {
                    hda.set({ deleted: true, purged: true });
                });
                xhr.fail((xhr, status, message) => {
                    // Exception messages are hidden within error page including:  '...not allowed in this Galaxy instance.'
                    // unbury and re-add to xhr
                    var error = _l("Unable to purge dataset");
                    var messageBuriedInUnfortunatelyFormattedError =
                        "Removal of datasets by users " + "is not allowed in this Galaxy instance";
                    if (xhr.responseJSON && xhr.responseJSON.error) {
                        error = xhr.responseJSON.error;
                    } else if (xhr.responseText.indexOf(messageBuriedInUnfortunatelyFormattedError) !== -1) {
                        error = messageBuriedInUnfortunatelyFormattedError;
                    }
                    xhr.responseText = error;
                    hda.trigger("error", hda, xhr, options, _l(error), {
                        error: error,
                    });
                });
                return xhr;
            },

            // ........................................................................ searching
            /** what attributes of an HDA will be used in a text search */
            searchAttributes: ["name", "file_ext", "genome_build", "misc_blurb", "misc_info", "annotation", "tags"],

            /** our attr keys don't often match the labels we display to the user - so, when using
             *      attribute specifiers ('name="bler"') in a term, allow passing in aliases for the
             *      following attr keys.
             */
            searchAliases: {
                title: "name",
                format: "file_ext",
                database: "genome_build",
                blurb: "misc_blurb",
                description: "misc_blurb",
                info: "misc_info",
                tag: "tags",
            },

            // ........................................................................ misc
            /** String representation */
            toString: function () {
                var nameAndId = this.get("id") || "";
                if (this.get("name")) {
                    nameAndId = `"${this.get("name")}",${nameAndId}`;
                }
                return `Dataset(${nameAndId})`;
            },
        }
    )
);

//==============================================================================
/** @class Backbone collection for dataset associations.
 */
var DatasetAssociationCollection = Backbone.Collection.extend(BASE_MVC.LoggableMixin).extend(
    /** @lends HistoryContents.prototype */ {
        _logNamespace: logNamespace,

        model: DatasetAssociation,

        /** root api url */
        urlRoot: `${getAppRoot()}api/datasets`,

        /** url fn */
        url: function () {
            return this.urlRoot;
        },

        // ........................................................................ common queries
        /** Get the ids of every item in this collection
         *  @returns array of encoded ids
         */
        ids: function () {
            return this.map((item) => item.get("id"));
        },

        /** Get contents that are not ready
         *  @returns array of content models
         */
        notReady: function () {
            return this.filter((content) => !content.inReadyState());
        },

        /** return true if any datasets don't have details */
        haveDetails: function () {
            return this.all((dataset) => dataset.hasDetails());
        },

        // ........................................................................ ajax
        /** using a queue, perform ajaxFn on each of the models in this collection */
        ajaxQueue: function (ajaxFn, options) {
            var deferred = jQuery.Deferred();
            var startingLength = this.length;
            var responses = [];

            if (!startingLength) {
                deferred.resolve([]);
                return deferred;
            }

            // use reverse order (stylistic choice)
            var ajaxFns = this.chain()
                .reverse()
                .map((dataset, i) => () => {
                    var xhr = ajaxFn.call(dataset, options);
                    // if successful, notify using the deferred to allow tracking progress
                    xhr.done((response) => {
                        deferred.notify({
                            curr: i,
                            total: startingLength,
                            response: response,
                            model: dataset,
                        });
                    });
                    // (regardless of previous error or success) if not last ajax call, shift and call the next
                    //  if last fn, resolve deferred
                    xhr.always((response) => {
                        responses.push(response);
                        if (ajaxFns.length) {
                            ajaxFns.shift()();
                        } else {
                            deferred.resolve(responses);
                        }
                    });
                })
                .value();
            // start the queue
            ajaxFns.shift()();

            return deferred;
        },

        // ........................................................................ sorting/filtering
        /** return a new collection of datasets whose attributes contain the substring matchesWhat */
        matches: function (matchesWhat) {
            return this.filter((dataset) => dataset.matches(matchesWhat));
        },

        /** String representation. */
        toString: function () {
            return ["DatasetAssociationCollection(", this.length, ")"].join("");
        },
    }
);

window.addEventListener("focus", function () {
    const testing = document.getElementById("tabicon");
    if (testing && testing.href.search("erricon") != -1) {
        TAB_UPDATES.change_favicon("static/favicon.ico");
    }
    document.title = "Galaxy";
    hiddenupdates = 1;
});
//==============================================================================
export default {
    DatasetAssociation: DatasetAssociation,
    DatasetAssociationCollection: DatasetAssociationCollection,
};
